🔒 Authentification centralisée avec Authelia et LLDAP
 Introduction
Les futures applications que nous installerons nécessiteront une authentification. Afin de faciliter la création ou la suppression de comptes utilisateurs, nous allons mettre une authentification centralisée.
Pour cela nous nous appuierons sur :
- L'annuaire léger LLDAP qui contiendra la base des comptes utilisateur
 - Le portail IAM Authelia permettant de fournir une solution d'authentification multi-facteur et unique par le biais du SSO.
 
Préparation
olivier@ds01:/mnt/nfsdatas$ mkdir stack-sso && chown olivier: stack-sso
olivier@ds01:/mnt/nfsdatas$ cd stack-sso
olivier@ds01:/mnt/nfsdatas/stack-sso$ mkdir -p config/authelia
olivier@ds01:/mnt/nfsdatas/stack-sso$ mkdir -p config/lldap/ssl
olivier@ds01:/mnt/nfsdatas/stack-sso$ mkdir env
olivier@ds01:/mnt/nfsdatas/stack-sso$ mkdir -p log/authelia
olivier@ds01:/mnt/nfsdatas/stack-sso$ mkdir -p secrets/authelia
olivier@ds01:/mnt/nfsdatas/stack-sso$ mkdir -p secrets/lldap
olivier@ds01:/mnt/nfsdatas/stack-sso$ mkdir -p data/{redis,data}
olivier@ds01:/mnt/nfsdatas/stack-sso$ tree -ld
.
├── config
│   ├── authelia
│   └── lldap
│       └── ssl
├── data
    ├── authelia
│   └── redis
├── env
├── log
│   └── authelia
└── secrets
    ├── authelia
    └── lldap
13 directories
networks:
  web:
    external: true
  sso:
    external: true
services:
  lldap:
    image: nitnelave/lldap:stable
    networks:
      - web
      - sso
    volumes:
      - ./config/lldap:/data
      - ./secrets/lldap:/secrets
    env_file:
      - ./env/container-vars-lldap.env
    deploy:
      placement:
        constraints:
          - node.role == worker
      restart_policy:
        condition: on-failure
      replicas: 1
      labels:
        - "traefik.enable=true"
        - "traefik.docker.network=web"
        - "traefik.http.routers.lldap.rule=Host(`l.dev.quercylibre.fr`)"
        - "traefik.http.routers.lldap.tls=true"
        - "traefik.http.routers.lldap.tls.certresolver=letsencrypt"
        # Le dashboard de LLDAP ne sera accessible qu'en local
        - "traefik.http.routers.lldap.middlewares=crowdsec@file,lldap-ipallowlist"
        - "traefik.http.middlewares.lldap-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.0/24"
        - "traefik.http.services.lldap.loadbalancer.server.port=17170"
        - "traefik.http.routers.lldap.service=lldap"
  authelia:
    image: authelia/authelia
    depends_on:
      - lldap
      - redis
    networks:
      - web
      - sso
    env_file:
      - ./env/container-vars-authelia.env
    volumes:
      - "./config/authelia:/config"
      - "./secrets/authelia:/secrets"
      - "./log/authelia:/var/log/authelia"
      - "./data/authelia:/data"
    environment:
      - TZ=Europe/Paris
    deploy:
      placement:
        constraints:
          - node.role == worker
      restart_policy:
        condition: on-failure
        delay: 45s
      replicas: 1
      labels:
        - "traefik.enable=true"
        - "traefik.docker.network=web"
        - "traefik.http.routers.authelia.rule=Host(`a.dev.quercylibre.fr`)"
        - "traefik.http.routers.authelia.tls=true"
        - "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
        - "traefik.http.routers.authelia.middlewares=crowdsec@file"
        - "traefik.http.services.authelia.loadbalancer.server.port=9091"
        - "traefik.http.routers.authelia.service=authelia"
  redis:
    image: redis:latest
    networks:
      - sso
    volumes:
      - ./data/redis:/data
    deploy:
      placement:
        constraints:
          - node.role == worker
      restart_policy:
        condition: on-failure
      replicas: 1
J'ai mis un délai de 45 secondes pour le reboot du conteneur Authelia car lors du lancement de la stack, le CT Authelia n'attend pas que LLDAP et Redis soient prêts. Par conséquent lors du premier boot, Authelia est en échec et attend 45 secondes avant de se relancer.
olivier@ds01:/mnt/nfsdatas/stack-sso$ docker network create --driver=overlay sso
Configuration de LLDAP
database_url = "sqlite:///data/users.db?mode=rwc"
# Commande pour générer la clé ci-dessous : tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1
key_seed = "O21vDuTDbXo69CimPb3G2FuyEYUuCc0E2A3UcXyUyuS2LxdtPCxKW0DiWfdkTKOs"
olivier@ds01:/mnt/nfsdatas/stack-sso$ openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout ./data/ssl/keyfile.key -out ./data/ssl/certfile.crt
olivier@ds01:/mnt/nfsdatas/stack-sso$ tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/lldap/JWT_SECRET
olivier@ds01:/mnt/nfsdatas/stack-sso$ tr -cd '[:alnum:]' < /dev/urandom | fold -w "20" | head -n 1 > ./secrets/lldap/LDAP_USER_PASS
LLDAP_LDAP_BASE_DN=dc=quercylibre,dc=fr
TZ=Europe/Paris
UID=1001
GID=1001
# If using LDAPS, set enabled true and configure cert and key path
LLDAP_LDAPS_OPTIONS__ENABLED=true
LLDAP_LDAPS_OPTIONS__CERT_FILE=/data/ssl/certfile.crt
LLDAP_LDAPS_OPTIONS__KEY_FILE=/data/ssl/keyfile.key
# Secrets: lldap reads them from the specified files.
# This way, the secrets are not part of any process environment.
LLDAP_JWT_SECRET_FILE=/secrets/JWT_SECRET
LLDAP_LDAP_USER_PASS_FILE=/secrets/LDAP_USER_PASS
Configuration d'Authelia
server:
  address: 'tcp://0.0.0.0:9091'
log:
  level: info
  format: text
  file_path: /var/log/authelia/authelia.log
theme: light
totp:
  algorithm: sha1
  digits: 6
  period: 30
  skew: 1
  secret_size: 32
password_policy: # Les mots de passe pourront être modifiés depuis Authelia mais selon les règles ci-dessous
  standard:
    enabled: false
    min_length: 14
    max_length: 0
    require_uppercase: true
    require_lowercase: true
    require_number: true
    require_special: true
  zxcvbn:
    enabled: false
    min_score: 3
authentication_backend:  
  password_reset:
    disable: false # Autorisation de modification des mots de passe par les utilisateurs
  refresh_interval: 5m
  # Configuration LDAP spécifique à LLDAP
  ldap:
    implementation: custom
    timeout: 5s
    start_tls: false
    additional_users_dn: 'ou=people'
    users_filter: "(&({username_attribute}={input})(objectClass=person))"
    additional_groups_dn: 'ou=groups'
    groups_filter: "(member={dn})"
    attributes:
      username: 'uid'
      display_name: 'displayName'
      mail: 'mail'
      group_name: 'cn'
access_control:
  default_policy: deny
  networks:
  - name: internal
    networks:
      - '192.168.1.0/24'
  rules:
    - domain: 
      - "cobalt.dev.quercylibre.fr" # Application d'extraction de vidéo et de son Youtubre, Instangram,...
      policy: 'one_factor' # Un seul facteur d'auth si connexion depuis le réseau interne 192.168.1.0/24 déclaré plus haut
      networks:
      - 'internal'
    - domain: 
      - "cobalt.dev.quercylibre.fr"
      policy: 'two_factor' # Deux facteurs d'auth si connexion depuis Internet ou un autre réseau.
    - domain:
      - "d.dev.quercylibre.fr" # Dashboard Traefik
      policy: 'two_factor'
      subject:
      - ['user:admin'] # Seul le compte admin aura accès au dashboard Traefik en remplacement du basic auth
session:
  name: authelia_session
  expiration: 12h  # 12 hours
  inactivity: 45m  # 45 minutes
  remember_me: 1M  # 1 month
  cookies:
    - domain: 'dev.quercylibre.fr'
      authelia_url: 'https://a.dev.quercylibre.fr'
      default_redirection_url: https://accueil.dev.quercylibre.fr
      name: 'authelia_session'
      same_site: 'lax'
      inactivity: '5m'
      expiration: '1h'
      remember_me: '1d'
  redis:
    host: redis 
    port: 6379 # port for REDIS docker contianer
    database_index: 0 # change this if you already use REDIS for something
regulation: # Blocage accès de 15 min au bout de 3 échecs d'auth en moins de 5 min.
  max_retries: 3
  find_time: 5m
  ban_time: 15m
notifier: 
  smtp:
    sender: "Authelia <authelia@mail.tld>" # Variable passée ici car bug si déclarée dans les variables d'env.
storage:
  # Il est possible d'utiliser d'autres back-end comme MariaDB ou PGSQL.
  local:
    path: /data/db.sqlite3
ntp:
  address: 'udp://time.cloudflare.com:123'
  version: 3
  max_desync: '3s'
  disable_startup_check: false
  disable_failure: false
PUID=1001
PGID=1001
AUTHELIA_TOTP_ISSUER=a.dev.quercylibre.fr
AUTHELIA_WEBAUTHN_DISPLAY_NAME=authelia
AUTHELIA_NOTIFIER_SMTP_ADDRESS=submission://<SERVER_MAIL>:587
AUTHELIA_NOTIFIER_SMTP_USERNAME=authelia@mail.tld
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDRESS=ldap://lldap:3890
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN=dc=quercylibre,dc=fr
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER=uid=admin,ou=people,dc=quercylibre,dc=fr
# Secrets: Authelia reads them from the specified files.
# This way, the secrets are not part of any process environment.
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/secrets/JWT_SECRET
AUTHELIA_SESSION_SECRET_FILE=/secrets/SESSION_SECRET
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/secrets/STORAGE_ENCRYPTION_KEY
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/secrets/NOTIFIER_SMTP_PASSWORD
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE=/secrets/AUTHENTICATION_BACKEND_LDAP_PASSWORD
olivier@ds01:/mnt/nfsdatas/stack-sso$ tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/authelia/JWT_SECRET
olivier@ds01:/mnt/nfsdatas/stack-sso$ tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/authelia/SESSION_SECRET
olivier@ds01:/mnt/nfsdatas/stack-sso$ tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 > ./secrets/authelia/STORAGE_ENCRYPTION_KEY
# Renseignez les mots de passe pour l'utilisateur SMT
olivier@ds01:/mnt/nfsdatas/stack-sso$ vim secrets/authelia/NOTIFIER_SMTP_PASSWORD
# Récupérez le mot de passe admin LLDAP
olivier@ds01:/mnt/nfsdatas/stack-sso$ cp secrets/LLDAP/LDAP_USER_PASS secrets/authelia/AUTHENTICATION_BACKEND_LDAP_PASSWORD
olivier@ds01:/mnt/nfsdatas/stack-sso$ chmod -R u+rwX,g-rwX,o-rwX secrets/
Déploiement de la stack
olivier@ds01:/mnt/nfsdatas/stack-sso$ docker stack deploy -c docker-stack.yml -d sso
Creating service sso_lldap
Creating service sso_authelia
Creating service sso_redis
olivier@ds01:/mnt/nfsdatas/stack-sso$ docker stack ps sso
ID             NAME                 IMAGE                      NODE      DESIRED STATE   CURRENT STATE            ERROR                       PORTS
hly93qjl1mcj   sso_authelia.1       authelia/authelia:latest   ds06      Running         Running 33 minutes ago
# Le premier fail ci-dessous est lié au fait qu'Authelia démarre sans attendre que LLDAP et Redis soient prêts                              
a2g5wssbh6w2    \_ sso_authelia.1   authelia/authelia:latest   ds06      Shutdown        Failed 34 minutes ago    "task: non-zero exit (1)"   
qkuih02x16ws   sso_lldap.1          nitnelave/lldap:stable     ds04      Running         Running 34 minutes ago                               
v23rog8bh9ah   sso_redis.1          redis:latest               ds04      Running         Running 34 minutes ago 
Configuration du middleware Authelia savec Traefik
http:
  middlewares:
  (...)
    authelia:
      forwardAuth:
        address: 'http://authelia:9091/api/authz/forward-auth'
        trustForwardHeader: true
        authResponseHeaders:
          - 'Remote-User'
          - 'Remote-Groups'
          - 'Remote-Email'
          - 'Remote-Name'
Déclaration du middleware pour le dashboard Traefik
L'objectif est de remplacer le basic auth de Traefik par Authelia et en restreigant l'accès au compte admin créé sur LLDAP grâce à la règle d'accès définie plus haut dans le fichier de configuration d'Authelia.
services:
  traefik:
    image: "traefik:v3.0.2"
    (...)
    deploy:
      placement:
        constraints:
          - node.role == manager
      restart_policy:
        condition: on-failure
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.traefik-dashboard.rule=Host(`d.dev.quercylibre.fr`)"
        - "traefik.http.routers.traefik-dashboard.service=api@internal"
        - "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
        - "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
        # Déclaration du middle-ware authelia@file
        - "traefik.http.routers.traefik-dashboard.middlewares=crowdsec@file,traefik-dashboard-ipallowlist,authelia@file"
        - "traefik.http.middlewares.traefik-dashboard-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.0/24"
        - "traefik.http.services.traefik-dashboard-service.loadbalancer.server.port=8080"
        (...)
On relance le service :
Il est quand même sacrément simple d'utiliser le middleware Authelia avec Traefik comparé à Nginx.
Configuration des méthodes 2FA
Outil pour le 2FA
Avant de configurer le 2FA, il vous faudra avoir un outil capable de gérer le 2FA. Personnellement, j'utilise Vaultwarden dont l'extension Firefox Bitwarden est capable de gérer le TOTP et les clés d'identification. Si vous avez déjà un conffre-fort pour vos mots de passe mais qui ne gère pas le TOTP, vous pouvez utiliser l'extension Firefox Authenticator.
Connectez vous à Authelia depuis un navigateur web puis authentifiez-vous avec un compte existant sur LLDAP.
Une fois authentifié, vous devez définir une méthode 2FA. Cliquez sur "Enregistrer l'appareil" :
Ajoutez une des deux méthodes. Ici je sélectionne la méthode basée sur un mot de passe à usage unique.
Un email vous est alors envoyé avec code temporaire.
Quand le code reçu par mail est saisi, Authelia vous invite alors à suivre la procédure ci-dessous afin d'activer le 2FA basé sur le TOTP.
Une fois la procédure terminée, la méthode est activée et visible dans les réglages Authelia accessible par l'utilisateur. Il est possible de le supprimer. Il vous sera alors envoyé de nouveau un code temporaire par email afin de confirmer la suppression.
Monitoring des services avec Uptime Kuma
Nous allons monitorer :
- Le service Authelia à travers son URL et son port 9091
 - Le service LLDAP à travers son URL et ses ports 17170 et 3890
 - Le service Redis à travers son port 6379
 
On déclare le réseau "sso" afin de permettre à Uptime Kuma de monitorer les services comme Redis.
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    volumes:
      - ./uptime-kuma-data:/app/data
    networks:
      - web
      - sso # Ici
    (...)
networks:
  web:
    external: true
  sso: # Et ici
    external: true
On relance le service.